#!/usr/bin/ruby
#
# Copyright:: Copyright 2012, Trimble Navigation Limited
# License:: All Rights Reserved.
# Original Author:: Scott Lininger (mailto:scott@sketchup.com)
#
# This file declares a new class for units conversion.
#
#     DCConverter  Provides unit conversion utilities for dynamic components

class DCConverter
  
  # Initialize the DCConverter object. Creates the all-critical data structures
  # that define the supported units and their converstion factors.
  #
  #  Args:
  #     None.
  def initialize
  
    # Create a Hash of supported attribute groups.
    @reserved_attribute_group = Hash[
      'name', 'STRING',
      'summary', 'STRING',
      'description', 'STRING',
      'itemcode', 'STRING',
      'x', 'LENGTH',
      'y', 'LENGTH',
      'z', 'LENGTH',
      'lenx', 'LENGTH',
      'leny', 'LENGTH',
      'lenz', 'LENGTH',
      'rotx', 'ANGLE',
      'roty', 'ANGLE',
      'rotz', 'ANGLE',
      'material', 'STRING',
      'hidden', 'BOOLEAN',
      'onclick', 'STRING',
      'copies', 'INTEGER',
      'imageurl', 'STRING',
      'dialogwidth', 'INTEGER',
      'dialogheight', 'INTEGER'
    ]

    # Create an array of Hashes, each describing one of the supported units.
    @units = Array[
      Hash[
        'name',       'DEFAULT',
        'label',      'User\'s default template units',
        'group',      'LENGTH',
        'configOnly', true
        ],
      Hash[
        'name',       'INTEGER',
        'label',      'Whole Number, no units',
        'group',      'INTEGER'
        ],
      Hash[
        'name',       'FLOAT',
        'label',      'Decimal Number, no units',
        'group',      'FLOAT'
        ],
      Hash[
        'name',       'PERCENT',
        'label',      'Percentage',
        'group',      'FLOAT',
        'test',       '^[\\d\\.,\\-\\s]+\\%[^\\w]*$',
        'template',   '{value}%',
        'conversion', 100.0
        ],
      Hash[
        'name',       'BOOLEAN',
        'label',      'True/False',
        'group',      'BOOLEAN',
        'test',       '^(true|false)$'
        ],
      Hash[
        'name',       'STRING',
        'label',      'Arbitrary Text, no units',
        'group',      'STRING'
        ],
      Hash[
        'name',       'INCHES',
        'label',      'Decimal Inches',
        'template',   '{value}&quot;',
        'group',      'LENGTH',
        'test',       '\\d\\s*\\"'
        ],
      Hash[
        'name',       'FEET',
        'label',      'Decimal Feet',
        'template',   "{value}'",
        'group',      'LENGTH',
        'conversion', 0.0833333333333,
        'test',       '^[\\d\\.,\\-\\s]+\\\'[^\\w]*$'
        ],
      Hash[
        'name',       'MILLIMETERS',
        'label',      'Millimeters',
        'template',   '{value} mm',
        'group',      'LENGTH',
        'conversion', 25.4,
        'test',       '^[\\d\\.,\\-\\s]+mm[^\\w]*$'
        ],
      Hash[
        'name',       'CENTIMETERS',
        'label',      'Centimeters',
        'template',   '{value} cm',
        'conversion', 2.54,
        'group',      'LENGTH',
        'test',       '^[\\d\\.,\\-\\s]+cm[^\\w]*$'
        ],
      Hash[
        'name',       'METERS',
        'label',      'Meters',
        'template',   '{value} m',
        'group',      'LENGTH',
        'conversion', 0.0254,
        'test',       '^[\\d\\.,\\-\\s]+m[^\\w]*$'
        ],
      Hash[
        'name',       'DEGREES',
        'label',      'Degrees',
        'template',   '{value}&deg;',
        'group',      'ANGLE'
        ],
      Hash[
        'name',       'DOLLARS',
        'label',      'Dollars',
        'group',      'CURRENCY',
        'test',       '\\$\\d'
        ],
      Hash[
        'name',       'EUROS',
        'label',      'Euros',
        'group',      'CURRENCY'
        ],
      Hash[
        'name',       'YEN',
        'label',      'Yen',
        'group',      'currency'
        ],
      Hash[
        'name',       'POUNDS',
        'label',      'Pounds (weight)',
        'template',   '{value} lbs',
        'group',      'WEIGHT',
        'test',       '\\d\\s*lbs'
        ],
      Hash[
        'name',       'KILOGRAMS',
        'label',      'Kilograms',
        'template',   '{value} kg',
        'group',      'WEIGHT',
        'conversion', 0.45359237,
        'test',       '\\d\\s*kg'
        ]
    ]

    # Create a friendly hash index into our @units, to allow us to use easy
    # lookups like conv.units_hash['CENTIMETERS']
    @units_hash = {}
    count = 0
    for unit in @units
      @units_hash[unit['name']] = unit
      count = count + 1
    end
    
    # Array of objects that describe the supported unit groups.
    @groups = [
      Hash[ 'name', 'STRING', 'label','Text', 'base', 'STRING' ],
      Hash[ 'name', 'LENGTH', 'label','Length', 'base', 'INCHES' ],
      Hash[ 'name', 'INTEGER', 'label','Whole Number', 'base', 'INTEGER' ],
      Hash[ 'name', 'FLOAT', 'label','Decimal Number', 'base', 'FLOAT' ],
      Hash[ 'name', 'ANGLE', 'label','Angle', 'base', 'DEGREES' ],
      Hash[ 'name', 'CURRENCY', 'label','Currency', 'base', 'DOLLARS' ],
      Hash[ 'name', 'WEIGHT', 'label','Weight', 'base', 'POUNDS' ],
      Hash[ 'name', 'BOOLEAN', 'label','Boolean', 'base', 'BOOLEAN' ]
    ]

    # Create a friendly hash index into our @groups, to allow us to use easy
    # lookups like conv.units_hash['INTEGER']
    @groups_hash = {}
    count = 0
    for group in @groups
      @groups_hash[group['name']] = group
      count = count + 1
    end
  end
  
  # Accessor for the units array.
  def units
    @units
  end

  # Accessor for the units_hash.
  def units_hash
    @units_hash
  end

  # Accessor for the groups array.
  def groups
    @groups
  end

  # Accessor for the groups_hash.
  def groups_hash
    @groups_hash
  end

  # Returns the base unit for a given reserved attribute
  def reserved_attribute_group
    @reserved_attribute_group
  end
  
  # Takes a string or number value and converts it into the appropriate base
  # units. For example, a call of to_base(25.4, 'MILLIMETERS') would return
  # 1.0, since inches are the base unit for millimeters.
  #  Args:
  #     value  The value to convert.
  #     units  The units name to convert from.
  # Returns:
  #     The converted value.
  def to_base(value, units)
    # If the value is nothing, then return nothing.
    if (value == nil || value == '')
      return ''
    end

    unit = units_hash[units];
    if (unit == nil || units == 'STRING')
      return clean_number(value)
    end

    # First see if there is a to_base function. If so, call that.
    function_name = units.downcase() + '_to_base';
    # TODO(Extensibility): The protected_methods collection is always empty for
    # all the testing I've done. Not sure if this is always true or not, but it
    # may be that this code serves no purpose. Evaluate this and determine if
    # this is true, and if so, remove it. If the code is used, it would be good
    # to wrap this check up into a static is_supported method.
    if self.protected_methods.include? function_name
      return self.send(function_name, value)
    end
    number = to_number(value)
    if (unit['conversion'] == nil)
      return number
    end
    return number / unit['conversion']

  end

  # Takes a string or number value and converts it into the passed units from
  # the appropriate base.
  #  Args:
  #     value  The value to convert.
  #     units  The units name to convert from.
  # Returns:
  #     The converted value.
  def from_base(value, units)
    unit = units_hash[units];
    if (unit == nil || units == 'STRING')
      return value
    end
    number = to_number(value)
    if (unit['conversion'] == nil)
      return number
    end
    return number * unit['conversion']
  end

  # Processes anything into a float.
  #  Args:
  #     value  The value to convert.
  # Returns:
  #     The value as a float.
  def to_number(value)
    # If we're passed a string, strip off any characters that are not digits,
    # periods, or minus signs.
    if value.kind_of? String
      # If the first character is anything but a digit, period, or a minus sign,
      # then this string is not convertable, so return 0.0. Otherwise, a 
      # string like "myPart!x/1000+75*99280" would parse down to 10007599280.
      if value =~ /^[^\d\.\-]/
        return 0.0
      end
      value = value.gsub(/[^\d\.\-]/, '')
    end
    
    value = value.to_f
    if value.to_s == "NaN"
      return 0.0
    else
      return value
    end
  end


  # If the element we're pulling in is a number, then clean up any
  # point rounding weirdness for purposes of display and comparison.
  #  Args:
  #     value  The value to convert.
  # Returns:
  #     The value as a string, containing number truncated to 6 decimal places,
  #     and stripped of trailing zeroes.
  def clean_number(value)
    if is_number(value)
      # NOTE - the .to_f call here is required so that the result is current
      #        after mathn from the Ruby Standard Library is loaded.
      value = (((value.to_f*1000000.0).round) / 1000000.0).to_f.to_s
      value = value.gsub(/\.0$/, '')
    end
    return value
  end

  # Tells us whether a passed value contains a parsable number.
  #  Args:
  #     value  The value to check.
  # Returns:
  #     true if the value as a string contains nothing but digits and decimals
  #       and the negative sign.
  def is_number(value)
    return value.to_s =~ /^\-*\d+\.*\d*$/
  end
end